package mns;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Date;

import redstone.xmlrpc.*;
import redstone.xmlrpc.handlers.ReflectiveInvocationHandler;
import redstone.xmlrpc.simple.Server;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.*;

public class ServiceEngine
{
	private redstone.xmlrpc.simple.Server rpcServer;

	private Connection sqlConn;
	
	private String moduleUnzipFolder = "/usr/MNS/gatechmns/src/";
	
	private HashMap<String, MNSInputModule> inputModules;
	private HashMap<String, MNSOutputModule> outputModules;

	private java.util.Timer serviceTimer;
	private SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
	public ServiceEngine(int rpcPort, String mysqlUsername, String mysqlPassword)
	{
		setupRpcServer(rpcPort);
		
		inputModules = new HashMap<String, MNSInputModule>();
		outputModules = new HashMap<String, MNSOutputModule>();
		
		try
		{
			Class.forName("com.mysql.jdbc.Driver").newInstance();
			sqlConn = DriverManager.getConnection("jdbc:mysql://localhost:3306/modularnotify_prod?user=" + 
					mysqlUsername + "&password=" + mysqlPassword);
		}
		catch(Exception e1)
		{
			System.err.println("Could not connect to database");
			System.err.println(e1);
			e1.printStackTrace();
			return;
		}		
		
		try
		{
			loadModules();
		}
		catch(SQLException e)
		{
			e.printStackTrace();
		}
	}
	
	public void start()
	{
		if(serviceTimer != null)
			return;
		
		
		TimerTask tt = new TimerTask()
		{
			public void run()
			{
				try
				{
					doNotifications();
				}
				catch(SQLException e)
				{
					e.printStackTrace();
				}
			}
		};
		
		serviceTimer = new Timer();
		serviceTimer.scheduleAtFixedRate(tt, 1000, 30000);
	}
		
	private boolean setupRpcServer(int rpcPort)
	{
		System.out.println("Starting XmlRpc server on port " + rpcPort);
		
		try
		{
			rpcServer = new Server(rpcPort);
		}
		catch(Exception e)
		{
			System.err.println("Could not instanciate XmlRpc Server:");
			System.err.println(e);
			e.printStackTrace();
			return false;
		}

		try
		{
			rpcServer.start();
		}
		catch(Exception e)
		{
			System.err.println("Could not start XmlRpc Server:");
			System.err.println(e);
			e.printStackTrace();
			return false;
		}

		XmlRpcServer server = rpcServer.getXmlRpcServer();

		XmlRpcHandler xrh = new XmlRpcHandler();
		ReflectiveInvocationHandler rih = new ReflectiveInvocationHandler(xrh);
		server.addInvocationHandler("ServiceEngine", rih);

		return true;
	}
	
	private void loadModules() throws SQLException
	{
		System.out.println("Loading Modules:");
		
		class loadItr
		{
			public void go(ResultSet rs) throws SQLException
			{
				while(rs.next())
				{
					String name = rs.getString("class_name");
					
					System.out.print("\t" + name);
					MNSModule m = loadModule(name);
					if(m != null)
					{
						System.out.println("\t(succeeded)");
						
						if(rs.getString("options_html") == null)
						{
							rs.updateString("options_html", m.getOptionsFormHTML());
							rs.updateRow();
						}
					}
					else
					{
						 System.out.println("\t(failed)");
					}
				}
			}
		}
		
		Statement stmt = sqlConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
		ResultSet rs = stmt.executeQuery("SELECT * FROM inputs");
		
		loadItr li = new loadItr();
		li.go(rs);
		rs.close();
		
		rs = stmt.executeQuery("SELECT * FROM outputs");
		li.go(rs);
		
		stmt.close();
		rs.close();
		System.out.println("Loading modules complete");
	}
	
	private MNSModule loadModule(String name) throws SQLException
	{
		if(inputModules.containsKey(name))
			return inputModules.get(name);
		if(outputModules.containsKey(name))
			return outputModules.get(name);
		
		MNSModule m = null;
		try
		{
			//Object o = Class.forName(name).getConstructor().newInstance();
			Object o = getObject(name);
			
			if(o instanceof MNSInputModule)
			{
				MNSInputModule im = (MNSInputModule)o;
				inputModules.put(name, im);
				m = im;
			}
			else if(o instanceof MNSOutputModule)
			{
				MNSOutputModule om = (MNSOutputModule)o;
				outputModules.put(name, om);
				m = om;
			}
			else
				return null;
		}
		catch(Exception e)
		{
			System.out.println();
			e.printStackTrace();
			return null;
		}
		
		return m;
	}
	
	private Object getObject(String className)
	{
		File classFile = findFile(className + ".class", new File(System.getProperty("user.dir")));
		if(classFile == null)
		{
			System.out.println("Cannot find classfile: " + className + ".class");
			return null;
		}
		
		URL url = null;
		try
		{
			url = classFile.getParentFile().toURI().toURL();
		}
		catch(MalformedURLException e)
		{
			e.printStackTrace();
		}
		URLClassLoader loader = new URLClassLoader(new URL[] {url});
		
		ClassOracle co = new ClassOracle(classFile.getAbsolutePath());
		
		Object o = null;
		try
		{
			o = loader.loadClass(co.getFQCN()).getConstructor().newInstance();
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}

		return o;
	}
	
	private File findFile(String name, File root)
	{
		for(File f : root.listFiles())
		{
			if(f.isFile())
			{
				if(f.getName().equals(name))
					return f;
			}
			else
			{
				File f2;
				if((f2 = findFile(name, f)) != null)
					return f2;
			}
		}		
		return null;		
	}
	
	public class XmlRpcHandler
	{
		public boolean loadModule(String name, String file_name)
		{
			/*
			 * modules zip/jar files must have the following folder structure:
			 * 
			 * 		mns/modules/input/module_name/
			 * or
			 * 		mns/modules/output/module_name/
			 *
			 * All the module's code should go into that folder
			 *
			 * This does imply that all the module's code must be in the following package:
			 * 
			 * 		mns.modules.input.module_name
			 * or
			 * 		mns.modules.output.module_name
			 */
			
			UnZip.unzip(file_name, moduleUnzipFolder);
			
			try {
				ServiceEngine.this.loadModules();
			} catch (SQLException e) {
				e.printStackTrace();
			}
			return true;
		}
	}
	
	private static final long millisecondsPerHour = 60*60*1000;
	private static final long millisecondsPerMinute = 60*1000;
	private void doNotifications() throws SQLException
	{
		Statement stmt = sqlConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
		ResultSet rs = stmt.executeQuery("SELECT * FROM user_modules ORDER BY next_notification LIMIT 0,30");
		long now = System.currentTimeMillis();
		System.out.printf("\nPolling notifications (%s):\n", dateFormatter.format(new Date(System.currentTimeMillis())));
		while(rs.next())
		{
			java.sql.Timestamp timestamp = rs.getTimestamp("next_notification");
			
			//pos is future, neg is past
			//if future:     bigger         //smaller
			long diff = timestamp.getTime() - now;
			//System.out.println(diff + ": " + (diff/millisecondsPerMinute));
			//System.out.println(Math.abs(diff) + " < " + millisecondsPerMinute);
			
			Notification n = new Notification(
					rs.getInt("user_id"),
					rs.getInt("input_id"),
					rs.getInt("output_id"),
					rs.getString("preferences"),
					timestamp,
					rs.getString("reschedule")
			);
			
			System.out.println("checking: " + n + ", diff = " + diff);
			
			if(Math.abs(diff) < millisecondsPerMinute) //now +/- 1 minute
			{
				Date d = doNotification(n);
				String date = dateFormatter.format(d);
				rs.updateString("next_notification", date);
				rs.updateRow();
			}
			else if(diff <  -millisecondsPerHour) //this notification is more than an hours old, "missed"
			{
				System.out.println("notificaiton older than an hour");
				Date d = rescheduleNotification(n);
				String date = dateFormatter.format(d);
				rs.updateString("next_notification", date);
				rs.updateRow();
			}
			else //"late" notification
			{
				
			}
		}
		
		rs.close();
		stmt.close();
	}
	
	private Date doNotification(Notification n) throws SQLException
	{
		System.out.println("notifying now");
		
		MNSInputModule in = getInputModule(n.getInputModuleId());
		MNSOutputModule out = getOutputModule(n.getOutputModuleId());
		UserOptions uo = new UserOptions();
		MNSMessage msg = in.getMessage(uo.fromXML(fixPrefs(n.getPrefString())));
		
		String outPrefs = getOutputModulePrefs(n.getUserId(), n.getOutputModuleId());
		UserOptions uo2 = new UserOptions();
		
		out.sendMessage(msg, uo2.fromXML(fixPrefs(outPrefs)));
		
		logNotification(n, msg);
		
		return rescheduleNotification(n);
	}
	
	private String fixPrefs(String prefs)
	{
		String p = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><ns2:userOptions xmlns:ns2=\"mns.options\">";
		p += prefs;
		p += "</ns2:userOptions>";
		return p;
	}
	
	private MNSOutputModule getOutputModule(int id) throws SQLException
	{
		Statement stmt = sqlConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
		ResultSet rs = stmt.executeQuery("SELECT class_name FROM outputs WHERE output_id=" + id);
		rs.next();
		MNSOutputModule m = outputModules.get(rs.getString("class_name"));
		rs.close();
		stmt.close();
		return m;
	}
	
	private String getOutputModulePrefs(int userId, int outputModuleId) throws SQLException
	{
		Statement stmt = sqlConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
		String sql = String.format(
				"SELECT preferences " +
				"FROM user_output_modules " +
				"WHERE user_id=%d AND output_id=%d", 
				userId, outputModuleId);

		ResultSet rs = stmt.executeQuery(sql);
		rs.next();
		
		String outPrefs = rs.getString("preferences");

		rs.close();
		stmt.close();
		
		return outPrefs;
	}
	
	private MNSInputModule getInputModule(int id) throws SQLException
	{
		Statement stmt = sqlConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
		ResultSet rs = stmt.executeQuery("SELECT class_name FROM inputs WHERE input_id=" + id);
		rs.next();
		MNSInputModule m = inputModules.get(rs.getString("class_name"));
		rs.close();
		stmt.close();
		return m;
	}
	
	private class Notification
	{
		private int userId, inputModuleId, outputModuleId;
		private String prefString;
		private java.sql.Timestamp notificationTime;
		private String reschedule;
		
		public Notification(int userId, int inputModuleId, int outputModuleId, String prefString, Timestamp notificationTime, String reschedule)
		{
			super();
			this.userId = userId;
			this.inputModuleId = inputModuleId;
			this.outputModuleId = outputModuleId;
			this.prefString = prefString;
			this.notificationTime = notificationTime;
			this.reschedule = reschedule;
		}
		
		public String toString()
		{
			return String.format("[userId=%d, inModId=%d, outModId=%d, notificationTime=%s, reschedule=%s]",
					userId, inputModuleId, outputModuleId, dateFormatter.format(notificationTime), reschedule);
		}
		
		public int getUserId()
		{
			return userId;
		}
		public int getInputModuleId()
		{
			return inputModuleId;
		}
		public int getOutputModuleId()
		{
			return outputModuleId;
		}
		public String getPrefString()
		{
			return prefString;
		}
		public java.sql.Timestamp getNotificationTime()
		{
			return notificationTime;
		}
		public String getReschedule()
		{
			return reschedule;
		}
	}
	
	private void logNotification(Notification n, MNSMessage msg) throws SQLException
	{
		String logmsgsql = null;
		Statement stmt = null;
		ResultSet rs = null;
		try
		{
			logmsgsql = String.format(
					"INSERT INTO messages (user_id, output_id, input_id, short_message, long_message, timestamp) " +
					"VALUES(%d, %d, %d, \"%s\", \"%s\", now())",
					n.getUserId(), n.getOutputModuleId(), n.getInputModuleId(), msg.getShortMessage().replaceAll("\"","\\\\\""),
					msg.getLongMessage().replaceAll("\"","\\\\\""));
			
			stmt = sqlConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
			stmt.executeUpdate(logmsgsql);
		}
		catch(Exception e)
		{
			System.err.println("Logging message failed: " + logmsgsql);
			e.printStackTrace();
		}
		
		//rs.close();
		stmt.close();
	}
	
	private Date rescheduleNotification(Notification n) throws SQLException
	{
		// MO,FR;13:00
		String[] reschedule = n.getReschedule().split(";");
		String[] days = reschedule[0].split(",");
		String[] time = reschedule[1].split(":");

		int nowWeekday = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
		
		int numDaysNextNotification = Integer.MAX_VALUE;
		if(days.length != 1)
		{
			for(String d : days)
			{
				int targetDay = dayToInt(d);
				if(nowWeekday != targetDay) //skip today
				{
					int numDays = targetDay - nowWeekday;
					System.out.println(numDays);
					if(numDays < 0)
					{
						numDays += 7;
					}
					
					if(numDays < numDaysNextNotification)
						numDaysNextNotification = numDays;
				}
			}
		}
		else //once a week
		{
			numDaysNextNotification = 7;
		}
		
		if(numDaysNextNotification == Integer.MAX_VALUE)
		{
			System.out.println("Couldn't reschedule: " + n);
			return n.getNotificationTime();
		}
		Calendar c = Calendar.getInstance();
		c.add(Calendar.DAY_OF_MONTH, numDaysNextNotification);
		c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(time[0]));
		c.set(Calendar.MINUTE, Integer.parseInt(time[1]));
		Date d = c.getTime();
		System.out.println("Rescheduling: " + n + " to " + dateFormatter.format(d));
		return d;
	}
	
	private int dayToInt(String day)
	{
		if("SUN".equals(day))
			return Calendar.SUNDAY;
		if("MO".equals(day))
			return Calendar.MONDAY;
		if("TU".equals(day))
			return Calendar.TUESDAY;
		if("WE".equals(day))
			return Calendar.WEDNESDAY;
		if("TH".equals(day))
			return Calendar.THURSDAY;
		if("FR".equals(day))
			return Calendar.FRIDAY;
		if("SA".equals(day))
			return Calendar.SATURDAY;
		
		return -1;
	}
	
	public static void main(String[] args) throws Exception
	{

		/*Connection conn = null;
		Statement stmt = null;
		ResultSet rs = null;

		Class.forName("com.mysql.jdbc.Driver").newInstance();

		conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql?" + "user=root&password=noobiesauc3");

		stmt = conn.createStatement();

		rs = stmt.executeQuery("SELECT * FROM help_topic LIMIT 0,30");

		while(rs.next())
		{
			System.out.println(rs.getString("name"));
		}

		rs.close();
		stmt.close();
		conn.close();*/
		
		ServiceEngine ne = new ServiceEngine(1234, "root", "noobiesauc3");
		ne.start();

	}
}
